<?php
/////////////////////////////////////////////////////////////////////////////
// 这个文件是 FleaPHP 项目的一部分
//
// Copyright (c) 2005 - 2006 FleaPHP.org (www.fleaphp.org)
//
// 要查看完整的版权信息和许可信息，请查看源代码中附带的 COPYRIGHT 文件，
// 或者访问 http://www.fleaphp.org/ 获得详细信息。
/////////////////////////////////////////////////////////////////////////////

/**
 * 定义 FLEA_Db_ORM 类
 *
 * @copyright Copyright (c) 2005 - 2006 FleaPHP.org (www.fleaphp.org)
 * @author 廖宇雷 dualface@gmail.com
 * @package Core
 * @version $Id: ORM.php 750 2007-02-26 08:44:25Z dualface $
 */

/**
 * FLEA_Db_ORM 实现了一个简单的 ORM 系统
 *
 * @package Core
 * @author 廖宇雷 dualface@gmail.com
 * @version 1.0
 */
class FLEA_Db_ORM
{
    /**
     * 对象和表数据入口类之间的映射信息
     *
     * @var array
     */
    var $_map = array();

    /**
     * 对象的实例
     *
     * @var array
     */
    var $_instances = array();

    /**
     * 表数据入口实例
     *
     * @var array
     */
    var $_tables = array();

    /**
     * 对象名和对象主键字段名之间的映射
     *
     * @var array
     */
    var $_idFields = array();

    /**
     * 对象的属性拷贝，用于检查对象是否已经发生了改变
     *
     * @var array
     */
    var $_shadow = array();

    /**
     * 保存查询对象时使用的参数，便于保存对象后重新查询对象数据
     *
     * @var array
     */
    var $_queryParameters = array();

    /**
     * 注册一个对象类及其对应表数据入口类的映射
     *
     * @param string $objClass
     * @param string $tableClass
     */
    function register($objClass, $tableClass)
    {
        $key = strtolower($objClass);
        $this->_map[$key] = array($objClass, $tableClass);
    }

    /**
     * 撤销对象类的注册，同时从 ORM 中移除该类的所有实例
     *
     * 移除实例不会导致对象被销毁。
     *
     * @param string $objClass
     */
    function unregister($objClass)
    {
        $key = strtolower($objClass);
        unset($this->_map[$key]);
        unset($this->_instances[$key]);
    }

    /**
     * 从 ORM 获取一个对象实例
     *
     * 如果 $enableLinks 参数为 true，则会从数据库查询该对象的关联数据，并实例化为对象。
     *
     * 对于关联的对象，会自动添加到 ORM 中。
     *
     * @param string $objClass
     * @param mixed $id
     * @param boolean $enableLinks
     *
     * @return object
     */
    function & get($objClass, $id = null, $enableLinks = true)
    {
        $key = strtolower($objClass);
        if (!isset($this->_map[$key])) {
            $result = false;
            return $result;
        }

        if (!isset($this->_tables[$key])) {
            list($objClass, $tableClass) = $this->_map[$key];

            FLEA::loadClass($objClass);
            FLEA::loadClass($tableClass);
            $table =& FLEA::getSingleton($tableClass);
            $this->_tables[$key] =& $table;
            $this->_idFields[$key] = $table->primaryKey;
        } else {
            $table =& $this->_tables[$key];
        }
        /* @var $table FLEA_Db_TableDataGateway */
        $idField = $this->_idFields[$key];

        if ($id) {
            $oldLinksStatus = $table->autoLink;
            $table->autoLink = $enableLinks;
            $data = $table->find($id);
            $table->autoLink = $oldLinksStatus;
        } else {
            $data = array();
        }

        // 构造对象并将数据设置为对象的属性
        $objId = $data[$idField];
        $this->_instances[$key][$objId] =& new $objClass;
        $this->attach($this->_instances[$key][$objId], $data, $enableLinks, $key);
        $shadowData =& $this->_getObjectProperties(
                $this->_instances[$key][$objId], $table->meta);
        $this->_shadow[$key][$objId] = serialize($shadowData);
        $this->_queryParameters[$key][$objId] = array('enableLinks' => $enableLinks);

        return $this->_instances[$key][$objId];
    }

    /**
     * 将一个数据库记录行附着到一个对象上
     *
     * 如果数据附着过程中产生了新对象，则新创建的对象会自动添加到 ORM 中。
     *
     * @param object $obj
     * @param array $data
     * @param boolean $enableLinks
     * @param string $tableKey
     */
    function attach(& $obj, & $data, $enableLinks = true, $tableKey = null)
    {
        if ($tableKey == null) {
            $tableKey = strtolower(get_class($obj));
        }
        $table =& $this->_tables[$tableKey];
        /* var $table FLEA_Db_TableDataGateway */

        $this->_setObjectProperties($obj, $table->meta, $data);
        if (!$enableLinks) { return; }

        // 处理关联
        foreach ($table->_links as $link) {
            /* @var $link FLEA_Db_TableLink */
            $fieldName = $link->mappingName;

            if (!isset($data[$fieldName]) || !is_array($data[$fieldName])) {
                $assocData = array();
            } else {
                $assocData =& $data[$fieldName];
            }

            if ($link->recordObjectClass == '') {
                // 如果关联没有指定对象名，则将关联数据直接作为对象的属性
                $obj->{$fieldName} = $assocData;
                continue;
            }

            $key = strtolower($link->recordObjectClass);
            if (!isset($this->_tables[$key])) {
                $assocTDG =& FLEA::getSingleton($link->tableClass);
                /* @var $assocTDG FLEA_Db_TableDataGateway */
                $this->_tables[$key] =& $assocTDG;
                $this->_idFields[$key] = $assocTDG->primaryKey;
                $idField = $assocTDG->primaryKey;
            } else {
                $assocTDG =& $this->_tables[$key];
                $idField = $this->_idFields[$key];
            }

            FLEA::loadClass($link->recordObjectClass);

            if ($link->oneToOne) {
                $objId = $assocData[$idField];
                $this->_instances[$key][$objId] =& new $link->recordObjectClass();
                $this->_setObjectProperties(
                        $this->_instances[$key][$objId],
                        $assocTDG->meta, $assocData);
                $shadowData =& $this->_getObjectProperties(
                        $this->_instances[$key][$objId],
                        $assocTDG->meta);
                $this->_shadow[$key][$objId] = serialize($shadowData);
                $this->_queryParameters[$key][$objId] = array('enableLinks' => false);
                $obj->{$fieldName} =& $this->_instances[$key][$objId];
            } else {
                $obj->{$fieldName} = array();
                foreach ($assocData as $assocItem) {
                    $objId = $assocItem[$idField];
                    $this->_instances[$key][$objId] =& new $link->recordObjectClass();
                    $this->_setObjectProperties(
                            $this->_instances[$key][$objId],
                            $assocTDG->meta, $assocItem);
                    $shadowData =& $this->_getObjectProperties(
                            $this->_instances[$key][$objId],
                            $assocTDG->meta);
                    $this->_shadow[$key][$objId] = serialize($shadowData);
                    $this->_queryParameters[$key][$objId] = array('enableLinks' => false);
                    $obj->{$fieldName}[] =& $this->_instances[$key][$objId];
                }
            }
        }
    }

    /**
     * 将对象保存到数据库
     *
     * @param object $obj
     * @param boolean $enableLinks
     *
     * @return boolean
     */
    function save(& $obj, $enableLinks = true)
    {
        $key = strtolower(get_class($obj));
        if (!isset($this->_map[$key])) {
            return false;
        }

        $idField = $this->_idFields[$key];
        $objId = $obj->{$idField};

        $table =& $this->_tables[$key];
        /* @var $table FLEA_Db_TableDataGateway */

        $shadowData =& $this->_getObjectProperties($obj, $table->meta);
        $serialize = serialize($shadowData);
        if ($this->_shadow[$key][$objId] == $serialize) {
            // 如果对象的数据没有发生变化，则无需保存到数据库
            return true;
        }

        // 从对象获取数据
        $data = array();
        foreach ($table->meta as $field) {
            $fieldName = $field['name'];
            $data[$fieldName] = $obj->{$fieldName};
        }

        if ($enableLinks) {
            // 处理对象关联的数据
            foreach ($table->_links as $link) {
                /* @var $link FLEA_Db_TableLink */
                $mappingName = $link->mappingName;
                if (!isset($obj->{$mappingName})) { continue; }
                if ($link->recordObjectClass == '') {
                    $data[$mappingName] = $obj->{$mappingName};
                    continue;
                }

                $key = strtolower($link->recordObjectClass);
                $assocTDG =& $this->_tables[$key];
                /* @var $assocTDG FLEA_Db_TableDataGateway */
                $data[$mappingName] = array();
                if ($link->oneToOne) {
                    $assocObject =& $obj->{$mappingName};
                    $assocObjId = $assocObject->{$this->_idFields[$key]};
                    $shadowData = $this->_getObjectProperties($assocObject, $assocTDG->meta);
                    $serialize = serialize($shadowData);
                    // 跳过没有改变的对象
                    if ($this->_shadow[$key][$assocObjId] == $serialize) { continue; }

                    foreach ($assocTDG->meta as $field) {
                        $fieldName = $field['name'];
                        $data[$mappingName][$fieldName] = $obj->{$mappingName}->{$fieldName};
                    }
                } else {
                    $offset = 0;
                    foreach ($obj->{$mappingName} as $assocObject) {
                        $assocObjId = $assocObject->{$this->_idFields[$key]};
                        $shadowData = $this->_getObjectProperties($assocObject, $assocTDG->meta);
                        $serialize = serialize($shadowData);
                        // 跳过没有改变的对象
                        if ($this->_shadow[$key][$assocObjId] == $serialize) { continue; }

                        foreach ($assocTDG->meta as $field) {
                            $fieldName = $field['name'];
                            $data[$mappingName][$offset][$fieldName] = $assocObject->{$fieldName};
                        }
                        $offset++;
                    }
                }
            }
        }

        $ret = $table->update($data);
        if ($ret == false) {
            return false;
        }

        /**
         * 重新读取保存后的对象数据，因为对象数据很有可能在保存到数据库时发生了变化
         *
         * 但在读取时，应该使用取出对象时使用的查询参数，
         * 避免对象保存后却和保存前多出来关联的数据。
         */
        $enableLinks = $this->_queryParameters[$key][$objId]['enableLinks'];
        $oldEnableLinks = $table->autoLink;
        $table->autoLink = $enableLinks;
        $data = $table->find(array($idField => $objId));
        $table->autoLink = $oldEnableLinks;

        $this->attach($obj, $table, $data, $enableLinks);
        return true;
    }

    /**
     * 保存 ORM 中的所有对象
     *
     * 所有修改过的对象都将被保存。
     */
    function saveAll()
    {
        $dboUsed = array();

        foreach ($this->_instances as $key => $objs) {
            $idField = $this->_idFields[$key];
            $offsetList = array_keys($objs);

            $table =& $this->_tables[$key];
            /* @var $table FLEA_Db_TableDataGateway */
            $oldEnableLinks = $table->autoLink;
            $table->autoLink = false;

            $dbo =& $table->getDBO();
            $dsn = serialize($dbo->dsn);
            if (!isset($dboUsed[$dsn])) {
                $dboUsed[$dsn] =& $dbo;
                $dbo->startTrans();
            }

            foreach ($offsetList as $offset) {
                $obj =& $objs[$offset];
                $objId = $obj->{$idField};

                $shadowData =& $this->_getObjectProperties($obj, $table->meta);
                $serialize = serialize($shadowData);
                if ($this->_shadow[$key][$objId] == $serialize) { continue; }
                $table->save($shadowData);
            }

            $table->autoLink = $oldEnableLinks;
        }

        $dboIDs = array_keys($dboUsed);
        foreach ($dboIDs as $dboID) {
            $dboUsed[$dboID]->completeTrans();
        }
    }

    /**
     * 添加一个对象到 ORM 中
     *
     * 要添加的对象类型必须是已经用 FLEA_Db_ORM::register() 注过的类型。
     *
     * @param object $obj
     */
    function add(& $obj)
    {
        $key = strtolower(get_class($obj));
        if (!isset($this->_map[$key])) {
            return;
        }

        if (!isset($this->_tables[$key])) {
            list($objClass, $tableClass) = $this->_map[$key];

            FLEA::loadClass($tableClass);
            $table =& FLEA::getSingleton($tableClass);
            $this->_tables[$key] =& $table;
            $this->_idFields[$key] = $table->primaryKey;
        }
        $idField = $this->_idFields[$key];
        if (!isset($obj->{$idField})) { return; }
        $objId = $obj->{$idField};

        $this->_instances[$key][$objId] =& $obj;
    }

    /**
     * 为对象设置属性
     *
     * @param object $obj
     * @param array $meta
     * @param array $data
     */
    function _setObjectProperties(& $obj, & $meta, & $data)
    {
        foreach ($meta as $field) {
            $fieldName = $field['name'];
            if (isset($data[$fieldName])) {
                $obj->{$fieldName} = $data[$fieldName];
            } else {
                $obj->{$fieldName} = null;
            }
        }
    }

    /**
     * 返回对象的属性
     *
     * @param object $obj
     * @param array $meta
     *
     * @return array
     */
    function & _getObjectProperties(& $obj, & $meta)
    {
        $data = array();
        foreach ($meta as $field) {
            $fieldName = $field['name'];
            $data[$fieldName] = $obj->{$fieldName};
        }
        return $data;
    }
}
